package edu.berkeley.nlp.util;

import static edu.berkeley.nlp.util.LogInfo.*;
import java.io.*;
import java.util.*;
import java.lang.annotation.*;
import java.lang.reflect.*;

class OptInfo {
	public String group, name, gloss;
	public String condReq;
	public boolean required;
	public boolean specified;
	public Object obj;
	// For serialization: sometimes the obj doesn't have enough information,
	// so we need to use the string that was used to construct the object
	public String stringRepn; // Used when obj is Random or BufferedReader (hard
								// to get string)

	// One of the following two are set
	public Field field;
	public Method setMethod, getMethod;

	public String fullName() {
		return group + "." + name;
	}

	// Return "" if field is not an enum type
	public String getEnumStr() {
		return getEnumStr(field != null ? field.getType() : getMethod
				.getReturnType());
	}

	public static String getEnumStr(Class c) {
		return StrUtils.join(c.getEnumConstants(), "|");
	}

	public Object getValue() {
		try {
			return field != null ? field.get(obj) : getMethod.invoke(obj);
		} catch (InvocationTargetException e) {
			stderr.println("Can't access method: " + e);
			return null;
		} catch (IllegalAccessException e) {
			stderr.println("Can't access field: " + e);
			return null;
		}
	}

	// Important to format properly in a way that we can read it and parse it
	// again.
	public String getValueString() {
		if (stringRepn != null)
			return stringRepn;
		Object o = getValue();
		// System.out.println("GOT " + fullName() + " " + o);
		if (o == null)
			return "";
		if (o instanceof ArrayList)
			return StrUtils.join((ArrayList) o);
		if (o instanceof Pair)
			return ((Pair) o).getFirst() + "," + ((Pair) o).getSecond();

		// Array
		if (objIsArray(o)) {
			StringBuilder buf = new StringBuilder();
			for (int i = 0; i < Array.getLength(o); i++) {
				if (i > 0)
					buf.append(' ');
				buf.append(Array.get(o, i));
			}
			return buf.toString();
		}
		if (o instanceof Random) // Argh, can't get the seed, just assume it's 1
			return "1";
		return o.toString();
	}

	public String toString() {
		String valueStr = getValueString();
		String s = String.format("%-30s <%5s> : %s [%s]", fullName(),
				typeStr(), gloss, valueStr);
		String t = getEnumStr();
		if (!t.equals(""))
			s += " " + t;
		return s;
	}

	public void print() {
		stdout.println("  " + toString());
	}

	private Type getGenericType() {
		return field != null ? field.getGenericType() : getMethod
				.getGenericReturnType();
	}

	private String typeStr() {
		return typeStr(getGenericType());
	}

	private static boolean isEnum(Type type) {
		return type instanceof Class && ((Class) type).isEnum();
	}

	// Array detectors
	static boolean objIsArray(Object o) {
		return typeIsArray(o.getClass());
	}

	static boolean typeIsArray(Type t) {
		return t instanceof Class && ((Class) t).getComponentType() != null;
	}

	static Class arrayTypeOfObj(Object o) {
		return arrayTypeOfType(o.getClass());
	}

	static Class arrayTypeOfType(Type t) {
		return (Class) ((Class) t).getComponentType();
	}

	private static boolean isBool(Type type) {
		return type.equals(boolean.class) || type.equals(Boolean.class);
	}

	private static String typeStr(Type type) {
		if (type.equals(boolean.class) || type.equals(Boolean.class))
			return "bool";
		if (type.equals(int.class) || type.equals(Integer.class))
			return "int";
		if (type.equals(short.class) || type.equals(Short.class))
			return "shrt";
		if (type.equals(double.class) || type.equals(Double.class))
			return "dbl";
		if (type.equals(String.class))
			return "str";
		if (type.equals(BufferedReader.class))
			return "read";
		if (type.equals(Random.class))
			return "rand";
		if (isEnum(type))
			return "enum";
		if (typeIsArray(type))
			return typeStr(arrayTypeOfType(type)) + "*";
		if (type instanceof ParameterizedType) {
			ParameterizedType ptype = (ParameterizedType) type;
			type = ptype.getRawType();
			Type[] childTypes = ptype.getActualTypeArguments();
			if (type.equals(ArrayList.class))
				return typeStr(childTypes[0]) + "*";
			if (type.equals(Pair.class))
				return typeStr(childTypes[0]) + "2";
		}
		return "unk";
	}

	private static boolean checkNumArgs(int want, int have, String fullName) {
		if (have != want) {
			stderr.printf(want + " arguments required for " + fullName
					+ ", but got " + have + "\n");
			return false;
		}
		return true;
	}

	// Return errorValue if there's an error (null is a valid value).
	// type: the data type of the variable
	// l: the command line arguments to interpret
	private static String errorValue = "ERROR";

	private static Object interpretValue(Type type, List<String> l,
			String fullName) {
		int n = l.size();
		String firstArg = n > 0 ? l.get(0) : null;

		if (type.equals(boolean.class) || type.equals(Boolean.class)) {
			boolean x = (n == 0 ? true : Boolean.parseBoolean(firstArg));
			return x;
		}
		if (type.equals(int.class) || type.equals(Integer.class)) {
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			int x;
			if (firstArg.equals("MAX"))
				x = Integer.MAX_VALUE;
			else if (firstArg.equals("MIN"))
				x = Integer.MIN_VALUE;
			else
				x = Integer.parseInt(firstArg);
			return x;
		}
		if (type.equals(short.class) || type.equals(Short.class)) {
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			short x;
			if (firstArg.equals("MAX"))
				x = Short.MAX_VALUE;
			else if (firstArg.equals("MIN"))
				x = Short.MIN_VALUE;
			else
				x = Short.parseShort(firstArg);
			return x;
		}
		if (type.equals(double.class) || type.equals(Double.class)) {
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			double x;
			if (firstArg.equals("MAX"))
				x = Double.POSITIVE_INFINITY;
			else if (firstArg.equals("MIN"))
				x = Double.NEGATIVE_INFINITY;
			else
				x = Double.parseDouble(firstArg);
			return x;
		}
		if (type.equals(double[].class)) {
			double[] x = new double[l.size()];
			for (int i = 0; i < l.size(); i++)
				x[i] = Double.parseDouble(l.get(i));
			return x;
		}
		if (type.equals(String[].class)) {
			String[] x = new String[l.size()];
			for (int i = 0; i < l.size(); i++)
				x[i] = l.get(i);
			return x;
		}
		if (type.equals(String.class)) { // Join many arguments using spaces
			String x = StrUtils.join(l);
			return x;
		}
		if (type.equals(BufferedReader.class)) {
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			BufferedReader x = "-".equals(firstArg) ? LogInfo.stdin : IOUtils
					.openInHard(firstArg);
			return x;
		}
		if (type.equals(Random.class)) {
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			// seed 0 means use the time
			int seed = Integer.parseInt(firstArg);
			Random x = seed == 0 ? new Random() : new Random(seed);
			return x;
		}
		if (type instanceof Class && ((Class) type).isEnum()) {
			if (n == 0)
				return null;
			if (!checkNumArgs(1, n, fullName))
				return errorValue;
			Object x = Utils.parseEnum((Class) type, firstArg);
			if (x == null) {
				stderr.println("Invalid enum: '" + firstArg
						+ "'; valid choices: " + getEnumStr((Class) type));
				return errorValue;
			}
			return x;
		}

		// Foo[], where Foo is any class
		if (typeIsArray(type)) {
			// Put the elements in the array
			Class childType = arrayTypeOfType(type);
			Object x = Array.newInstance(childType, l.size());
			int i = 0;
			for (String a : l) {
				Object o = interpretValue(childType, ListUtils.newList(a),
						fullName);
				if (o == errorValue)
					return errorValue;
				Array.set(x, i++, o);
			}
			return x;
		}

		// Pair or ArrayList
		if (type instanceof ParameterizedType) {
			// Types involving generics: pair, arraylist
			ParameterizedType ptype = (ParameterizedType) type;
			type = ptype.getRawType();
			Type[] childTypes = ptype.getActualTypeArguments();

			if (type.equals(Pair.class)) { // Delimited by comma
				if (!checkNumArgs(1, n, fullName))
					return errorValue;
				// Put the elements in the array
				String[] tokens = firstArg.split(",", 2);
				if (tokens.length != 2) {
					stderr.println("Invalid pair: '" + firstArg + "'");
					return errorValue;
				}
				Object o1 = interpretValue(childTypes[0], ListUtils
						.newList(tokens[0]), fullName);
				if (o1 == errorValue)
					return errorValue;
				Object o2 = interpretValue(childTypes[1], ListUtils
						.newList(tokens[1]), fullName);
				if (o2 == errorValue)
					return errorValue;
				return new Pair(o1, o2);
			} else if (type.equals(List.class) || type.equals(ArrayList.class)) {
				ArrayList x = new ArrayList();
				// Put the elements in the array
				for (String a : l) {
					Object o = interpretValue(childTypes[0], ListUtils
							.newList(a), fullName);
					if (o == errorValue)
						return errorValue;
					x.add(o);
				}
				return x;
			}
		}

		// Try to construct the weird type using the constructor
		// that takes one string argument.
		if (type instanceof Class) {
			try {
				Constructor con = ((Class) type).getConstructor(String.class);
				return con.newInstance(new Object[] { StrUtils.join(l) });
			} catch (Exception e) {
				stderr.println("Failed to construct " + type + ": " + e);
				e.printStackTrace();
				return errorValue;
			}
		}

		stderr.println("Can't handle weird field type: " + type);
		return errorValue;
	}

	private void setField(Object v) throws IllegalAccessException,
			InvocationTargetException {
		if (!tryToUseSetters(v)) {
			if (field != null)
				field.set(obj, v);
			else
				setMethod.invoke(obj, v);
		}
	}

	private boolean tryToUseSetters(Object v) {
		if (field == null)
			return false;
		String targetMethodName = "set" + field.getName();
		Method[] methods = obj.getClass().getMethods();
		for (Method m : methods) {
			String methodName = m.getName().toLowerCase();
			if (methodName.equalsIgnoreCase(targetMethodName)) {
				try {
					m.invoke(obj, v);
				} catch (Exception e) {
					return false;
				}
				return true;
			}
		}
		return false;
	}

	public boolean set(List<String> l, boolean append) {
		try {
			Object v = interpretValue(getGenericType(), l, fullName());
			if (v == errorValue)
				return false;
			// System.out.println(name + " " + stringRepn + " " + v);
			if (!append) {
				// Treat boolean case specially because -flag means true, and l
				// is empty
				if (isBool(getGenericType()))
					stringRepn = v.toString();
				else
					stringRepn = StrUtils.join(l);
				// field.set(obj, v);
				setField(v);
			} else {
				Object oldv = field.get(obj);
				// System.out.println("append " + l);
				// System.out.println((oldv == null ? "" : (String)oldv + " ") +
				// v);
				stringRepn = (stringRepn == null ? "" : stringRepn + " ")
						+ StrUtils.join(l);
				if (oldv instanceof ArrayList)
					((ArrayList) oldv).addAll((ArrayList) v);
				else if (oldv instanceof String)
					// field.set(obj, (oldv == null ? "" : (String)oldv + " ") +
					// v);
					setField((oldv == null ? "" : (String) oldv + " ") + v);
			}
		} catch (InvocationTargetException e) {
			stderr.println("Can't set method: " + e);
			return false;
		} catch (IllegalAccessException e) {
			stderr.println("Can't set field: " + e);
			return false;
		}

		specified = true;
		return true;
	}
}

/**
 * Due to historical reasons, all the member functions are prefixed with do, and
 * all the static functions (apply to the global theParser instance) are not.
 * 
 * 3/1/2007: static methods register and parse have been deprecated. Please
 * create an instance and use the doRegister and doParse counterparts.
 */
public class OptionsParser {
	public OptionsParser() {
	}

	public OptionsParser(Object... objects) {
		doRegisterAll(objects);
	}

	public OptionsParser doRegister(String group, Object o) {
		if (objects.containsKey(group))
			throw Exceptions.bad("Group name already exists: " + group);
		objects.put(group, o);

		// Recursively register its option sets
		for (Field field : classOf(o).getFields()) {
			OptionSet ann = (OptionSet) field.getAnnotation(OptionSet.class);
			if (ann == null)
				continue;
			try {
				doRegister(group + "." + ann.name(), field.get(o));
			} catch (IllegalAccessException e) {
				throw Exceptions.bad("Can't access field: " + e);
			}
		}

		for (Method method : classOf(o).getMethods()) {
			OptionSet ann = (OptionSet) method.getAnnotation(OptionSet.class);
			if (ann == null)
				continue;
			try {
				doRegister(group + "." + ann.name(), method.invoke(o));
			} catch (InvocationTargetException e) {
				throw Exceptions.bad("Can't access method: " + e);
			} catch (IllegalAccessException e) {
				throw Exceptions.bad("Can't access method: " + e);
			}
		}

		return this;
	}

	public OptionsParser doRegisterAll(Object[] objects) {
		// Strings are interpreted as the key name for the next object.
		String name = null;
		for (Object o : objects) {
			if (o == null)
				continue;
			if (o instanceof String)
				name = (String) o;
			else {
				if (name == null) {
					if (o instanceof Class)
						name = ((Class) o).getSimpleName();
					else
						name = o.getClass().getSimpleName();
				}
				doRegister(name, o);
				name = null;
			}
		}
		return this;
	}

	@Deprecated
	// Don't use the static methods
	public static void register(String group, Object o) {
		theParser.doRegister(group, o);
	}

	@Deprecated
	// Don't use the static methods
	public static void registerAll(Object[] objects) {
		theParser.doRegisterAll(objects);
	}

	private static Class classOf(Object o) {
		return (o instanceof Class) ? (Class) o : o.getClass();
	}

	private List<OptInfo> matchOpt(ArrayList<OptInfo> options, String s,
			boolean allowMultipleMatches) {
		s = s.toLowerCase();

		ArrayList<OptInfo> completeMatches = new ArrayList<OptInfo>();
		ArrayList<OptInfo> partialMatches = new ArrayList<OptInfo>();
		for (OptInfo opt : options) {
			String t;

			// First try to match full name
			t = opt.fullName().toLowerCase();
			if (t.equals(s))
				completeMatches.add(opt);
			if (t.startsWith(s))
				partialMatches.add(opt);

			// Otherwise, match name (without the group)
			if (!mustMatchFullName) {
				t = opt.name.toLowerCase();
				if (t.equals(s))
					completeMatches.add(opt);
				if (t.startsWith(s))
					partialMatches.add(opt);
			}
		}

		if (completeMatches.size() + partialMatches.size() == 0) {
			if (!ignoreUnknownOpts)
				stderr.println("Unknown option: '" + s + "'; -help for usage");
			return ListUtils.newList();
		}

		if (allowMultipleMatches)
			return partialMatches;
		else {
			// Enforce one match
			if (completeMatches.size() == 1)
				return ListUtils.newList(completeMatches.get(0));
			if (completeMatches.size() == 0 && partialMatches.size() == 1)
				return ListUtils.newList(partialMatches.get(0));

			stderr.println("Ambiguous option: '" + s + "'; possible matches:");
			for (OptInfo opt : partialMatches)
				opt.print();
			return ListUtils.newList();
		}
	}

	private static void printHelp(List<OptInfo> options) {
		stdout.println("Usage:");
		for (OptInfo opt : options)
			opt.print();
	}

	public void printHelp() {
		printHelp(options);
	}

	private ArrayList<OptInfo> getOptInfos() {
		ArrayList<OptInfo> options = new ArrayList<OptInfo>();

		// For each group...
		for (String group : objects.keySet()) {
			Object obj = objects.get(group);

			// For each field that has an option annotation...
			// for(Field field : classOf(obj).getDeclaredFields()) {
			for (Field field : classOf(obj).getFields()) {
				Option ann = (Option) field.getAnnotation(Option.class);
				if (ann == null)
					continue;

				// Get the option
				OptInfo opt = new OptInfo();
				opt.group = group;
				opt.name = ann.name().equals("") ? field.getName() : ann.name();
				opt.gloss = ann.gloss();
				opt.condReq = ann.condReq();
				opt.required = ann.required();
				opt.obj = obj;
				opt.field = field;
				options.add(opt);

				// System.out.println("OPT " + opt.name);
			}

			// In Scala, "@Option var x" generates two methods
			// a setter and a getter
			// public int Options.x()
			// public void Options.x_$eq(int)

			// Map getter method name to the option
			HashMap<String, OptInfo> optMap = new HashMap();
			for (Method method : classOf(obj).getMethods()) {
				Option ann = (Option) method.getAnnotation(Option.class);
				if (ann == null)
					continue;

				// System.out.println("OPT " + method);
				String getterName = method.getName().replace("_$eq", "");
				OptInfo opt = optMap.get(getterName);
				if (opt == null) {
					opt = new OptInfo();
					opt.group = group;
					opt.name = ann.name().equals("") ? method.getName() : ann
							.name();
					opt.gloss = ann.gloss();
					opt.condReq = ann.condReq();
					opt.required = ann.required();
					opt.obj = obj;
					options.add(opt);
					optMap.put(getterName, opt);
				}

				// Get the option
				if (method.getName().endsWith("_$eq")) // setter
					opt.setMethod = method;
				else
					// getter
					opt.getMethod = method;
			}
		}

		for (OptInfo opt : options) {
			if (!(opt.field != null || (opt.getMethod != null && opt.setMethod != null)))
				System.err
						.printf(
								"%s must have either field or a getter/setter pair (probably missing setter; use var instead of val in Scala)\n",
								opt.fullName());
		}

		return options;
	}

	// Options file: one option per line
	// Key and value separated by tab (or spaces).
	private boolean readOptionsFile(ArrayList<OptInfo> options, String file) {
		if (new File(file).isDirectory())
			file = new File(file, defaultDirFileName).toString();
		boolean ignoreOpts = new File(file).getName()
				.equals(ignoreOptsFileName);

		try {
			// OrderedStringMap map = OrderedStringMap.fromFile(file);
			// {12/06/08}: Allow spaces

			BufferedReader in = IOUtils.openIn(file);
			String line;
			while ((line = in.readLine()) != null) {
				line = line.trim();
				if (line.length() == 0 || line.startsWith("#"))
					continue;
				String[] tokens = line.split("\\s+", 2);
				String key = tokens[0];
				String val = (tokens.length > 1 ? tokens[1] : "");

				boolean append = false;

				if (key.startsWith("+")) {
					append = true;
					key = key.substring(1);
				}

				if (key.equals("!include")) { // Include other file
					if (!readOptionsFile(options, val))
						return false;
				} else {
					for (OptInfo opt : matchOpt(options, key, false)) {
						if (ignoreOpts
								&& ignoreFileNameOpts.contains(opt.fullName()))
							continue;
						if (!opt
								.set(Arrays.asList(StrUtils.split(val)), append))
							return false;
					}
				}
			}
		} catch (IOException e) {
			stderr.println(e);
			return false;
		}
		return true;
	}

	public boolean parseOptionsFile(String path) {
		ArrayList<OptInfo> options = getOptInfos();
		return readOptionsFile(options, path);
	}

	// Return true iff x is a strict prefix of
	private static boolean isStrictPrefixOf(String x, String... ys) {
		for (String y : ys)
			if (x.startsWith(y) && x.length() > y.length())
				return true;
		return false;
	}

	private static String stripDashes(String s) {
		int i = 0;
		while (i < s.length() && (s.charAt(i) == '-' || s.charAt(i) == '+'))
			i++;
		return s.substring(i);
	}

	@Deprecated
	public static boolean parse(String[] args) {
		return theParser.doParse(args);
	}

	public void doParseHard(String[] args) {
		if (!doParse(args))
			throw new RuntimeException("Parsing '" + StrUtils.join(args)
					+ "' failed");
	}

	public boolean doParse(String[] args) {
		if (this.options == null)
			this.options = getOptInfos();

		// For each command-line argument...
		for (int i = 0; i < args.length;) {
			if (args[i].equals("-help")) { // Get usage help
				printHelp(options);
				i++;
				return false;
				// if(!ignoreUnknownOpts) continue;
				// else return false;
			} else if (isStrictPrefixOf(args[i], "++")) {
				if (!readOptionsFile(options, args[i++].substring(2))) {
					if (ignoreUnknownOpts)
						continue;
					else
						return false;
				}
			} else if (isStrictPrefixOf(args[i], "-", "+", "--")) {
				boolean append = args[i].startsWith("+");
				boolean allowMultipleMatches = args[i].startsWith("--");
				// System.err.println(allowMultipleMatches + " " + args[i]);
				List<OptInfo> opts = matchOpt(options, stripDashes(args[i++]),
						allowMultipleMatches);

				// Get the data values of this parameter
				ArrayList<String> l = new ArrayList<String>();
				boolean nextIsVerbatim = false;
				boolean allIsVerbatim = false;
				while (i < args.length) {
					if (args[i].equals("--"))
						nextIsVerbatim = true;
					else if (args[i].equals("---"))
						allIsVerbatim = !allIsVerbatim;
					else {
						if (!allIsVerbatim && !nextIsVerbatim
								&& (isStrictPrefixOf(args[i], "+", "-", "++")))
							break;
						l.add(args[i]);
						nextIsVerbatim = false;
					}
					i++;
				}

				if (opts.size() == 0 && !ignoreUnknownOpts)
					return false;
				for (OptInfo opt : opts) {
					if (!opt.set(l, append)) {
						if (ignoreUnknownOpts)
							continue;
						else
							return false;
					}
				}
			} else {
				stderr.println("Argument not part of an option: " + args[i]);
				if (!ignoreUnknownOpts)
					return false;
			}
		}

		// Check that all required options are specified
		if (!relaxRequired) {
			List<String> missingOptMsgs = new ArrayList<String>();
			for (OptInfo o : options) {
				String msg = isMissing(o, options);
				if (msg != null)
					missingOptMsgs.add(msg);
			}
			if (missingOptMsgs.size() > 0) {
				stderr.println("Missing required option(s):");
				for (String msg : missingOptMsgs)
					stderr.println(msg);
				return false;
			}
		}

		return true;
	}

	// Return the option info with the given name (which could be full or not).
	// If not, then prepend the given group.
	private OptInfo findOptInfo(List<OptInfo> optInfos, String name,
			String group) {
		for (OptInfo info : optInfos)
			if (info.fullName().equals(name))
				return info;
		name = group + "." + name;
		for (OptInfo info : optInfos)
			if (info.fullName().equals(name))
				return info;
		return null;
	}

	// If the option is missing, return the message (to be printed out) of why
	// Otherwise, return null
	private String isMissing(OptInfo o, List<OptInfo> optInfos) {
		if (o.specified)
			return null; // Specified, we're fine
		if (o.required)
			return o.toString(); // This option is required
		if (!StrUtils.isEmpty(o.condReq)) {
			// This option is conditionally required
			String[] tokens = o.condReq.split("=", 2);
			String name = tokens[0], value = tokens.length == 2 ? tokens[1]
					: null;
			OptInfo info = findOptInfo(optInfos, name, o.group);
			boolean missing;
			if (info == null) // Shouldn't happen, but if it does, the user will
								// be notified
				return o.toString() + ", " + name + " not found";
			else if (value == null) { // Just need to be specified
				if (info.specified)
					return o.toString() + ", " + name + " specified";
			} else {
				if (info.getValue() instanceof ArrayList) { // For an array,
															// suffices if just
															// one element
															// matches
					for (Object x : (ArrayList) info.getValue())
						if (x.toString().matches(value))
							return o.toString() + ", " + o.condReq + " holds";
				} else {
					if (info.getValueString().matches(value))
						return o.toString() + ", " + o.condReq + " holds";
				}
			}
		}
		return null;
	}

	// Return a list of options (verbose - human-readable)
	@Deprecated
	public static OrderedStringMap getOptionStrings() {
		return theParser.doGetOptionStrings();
	}

	public OrderedStringMap doGetOptionStrings() {
		if (this.options == null)
			this.options = getOptInfos();
		OrderedStringMap map = new OrderedStringMap();
		for (OptInfo opt : options)
			map.put(opt.toString());
		return map;
	}

	// Return a list of option pairs (mapping name to value)
	@Deprecated
	public static OrderedStringMap getOptionPairs() {
		return theParser.doGetOptionPairs();
	}

	public OrderedStringMap doGetOptionPairs() {
		if (this.options == null)
			this.options = getOptInfos();
		OrderedStringMap map = new OrderedStringMap();
		for (OptInfo opt : options)
			map.put(opt.fullName(), opt.getValueString());
		return map;
	}

	public boolean writeEasy(String path) {
		return doGetOptionPairs().printEasy(path);
	}

	public OptionsParser setDefaultDirFileName(String defaultDirFileName) {
		this.defaultDirFileName = defaultDirFileName;
		return this;
	}

	public OptionsParser setIgnoreOptsFromFileName(String ignoreOptsFileName,
			List<String> ignoreFileNameOpts) {
		this.ignoreOptsFileName = ignoreOptsFileName;
		this.ignoreFileNameOpts = ignoreFileNameOpts;
		return this;
	}

	public OptionsParser relaxRequired() {
		this.relaxRequired = true;
		return this;
	}

	public OptionsParser ignoreUnknownOpts() {
		this.ignoreUnknownOpts = true;
		return this;
	}

	public OptionsParser mustMatchFullName() {
		this.mustMatchFullName = true;
		return this;
	}

	// public String getHotSpec() { return hotSpec; }

	// Each object could either be a class or an object.
	private HashMap<String, Object> objects = new HashMap<String, Object>();
	private ArrayList<OptInfo> options;
	// private String hotSpec;

	// Settings for parsing
	private String defaultDirFileName; // If ++<dir> is specified, read from
										// <dir>/<defaultDirFileName>
	private String ignoreOptsFileName; // If reading a file with this file
										// name...
	private List<String> ignoreFileNameOpts; // ignore these options

	private boolean relaxRequired; // Forget about having to have all options
	private boolean ignoreUnknownOpts; // Don't stop parsing if have error
	private boolean mustMatchFullName; // Must include group and name

	@Deprecated
	public static final OptionsParser theParser = new OptionsParser();
}
